# encoding=utf-8
"""
Markdown及扩展语法解析转换HTML的工具

1. python-markdown转换html
    三方库安装：
        python -m pip install Pygments -i https://pypi.tuna.tsinghua.edu.cn/simple
        python -m pip install markdown -i https://pypi.tuna.tsinghua.edu.cn/simple
        python -m pip install pymdown-extensions -i https://pypi.tuna.tsinghua.edu.cn/simple
        python -m pip install markdown-del-ins -i https://pypi.tuna.tsinghua.edu.cn/simple
    复杂网格表格库安装：[--install-lib=xxx\\Lib\\site-packages]
        cd resource/Markdown-GridTables; python .\\setup.py install
    pymdown-extensions使用：https://facelessuser.github.io/pymdown-extensions/

2. tkinter浏览网页&展示html
    三方库安装：
        python -m pip install pywin32 -i https://pypi.tuna.tsinghua.edu.cn/simple
    miniblink的python绑定下载安装：
        pypi下载 mbpython 然后修改setup.py，将win32的依赖修改一下(原始依赖228<=x<229)
        cd .\\MBPython-0.2.2; python .\\setup.py install
    下载 miniblink_x64.dll：
        https://github.com/weolar/miniblink49/releases 下载 miniblink_x64.dll

3. pyecharts图表
    python -m pip install pyecharts -i https://pypi.tuna.tsinghua.edu.cn/simple

4. mermaid语法支持
    本地预览使用离线mermaid.min.js，渲染时替换为CDN链接
"""

import sys
import os
import re
import time
import json
import shutil
import threading
import webbrowser
from collections import OrderedDict
import tkinter as tk
from tkinter import filedialog, messagebox
import customtkinter as ctk
import markdown
from mbpython.miniblink import Miniblink
from PIL import Image
from pyecharts.charts import Bar, Line, Pie, WordCloud, Liquid
from pyecharts import options as opts
from pyecharts.globals import SymbolType, ThemeType

VERSION = '1.0.3'
MD_TEMPLATES = {
        '标题': """
# 一级标题（默认居中）
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题
""",
        '粗体': "\n**粗体文本**  ",
        '斜体': "\n*斜体文本*  ",
        '斜体加粗': "\n***斜体加粗文本***  ",
        '删除文字': "\n~~删除文本~~  ",
        '下划线': "\n++下划线++  ",
        '背景高亮': "\n==背景高亮==  ",
        '引用': "\n> 引用\n\n",
        '代码块': """\n
```python
import os
print(os.name)
```

```python title="test.py" hl_lines="2"
import os
print(os.name)
```
""",
        '行代码': "\n`ls -la`\n",
        '空行': "\n<br>\n",
        '表格': """\n
|A|B|
|--|--|
|1|2|
|a|b|
""",
        '复杂表格': """\n
+--------+--------------------+
|  Table Headings             |
+========+====================+
| 简单单元格  | 每个表格中还可以有特殊效果      |
+--------+--------------------+
| 多行文本   | 每列的字符加空格数量要一致，否则会出错|
| 单元格    | 汉字算一个字符，效果字符也算字符数  |
+--------+--------------------+
|        | https://baidu.com  |
+        +--------------------+
| 合并的单元格 | `行内代码` ==背景高亮==    |
+        +--------------------+
|        | **加粗文本** ~~删除线~~   | 
+--------+--------------------+
""",
        '有序列表': """\n
1. 第一行
2. 第二行
3. 第三行
""",
        '无序列表': """\n
- 第一行
- 第二行
- 第三行
""",
        '任务列表': """\n
- [x] 支持aaa
- [x] 支持bbb
- [ ] 不支持zzz
""",
        '提示': """
/// tip | xxx信息
提示内容：

- xxx
- yyy
///
""",
        '注意': """
/// note | xxx注意
注意内容：

- xxx
- yyy
///
""",
        '警告': """
/// warning | xxx警告
警告内容：

- xxx
- yyy
///
""",
        '错误': """
/// error | xxx错误
错误内容：

- xxx
- yyy
///
""",
        '水平线': "\n---\n",
        '进度条': '''\n[=80% "80%"]''',
        '折叠块': """
/// details | 展开查看详情
    attrs: {class: expand-box}
**XXX**

```python
import os
```
///
/// details | 默认展开
    attrs: {class: expand-container}
    open: true
```python
import os
```
///
""",
        '标签页': """
/// tab | AAA
**AAA**

```python
import os
```
///
/// tab | BBB
**BBB**

```python
import sys
```
///
""",
        '图片': "\n![](%s)\n",
        '按钮': """\n<a href="https://www.baidu.com"><button>跳转百度</button></a>\n""",
        '中心对齐': """\n<p style="text-align: center;">居中对齐文本</p>\n""",
        '目录': "\n[TOC]\n",
        '脚注': """
这是一个脚注[^1]

[^1]: 这是脚注的细节
""",
        'Mermaid': """
**mermaid语法: http://mermaid.js.org/**

```mermaid
sequenceDiagram
    Alice->>John: Hello John, how are you?
    John-->>Alice: Great!
    Alice-)John: See you later!
```
""",
        'Echarts': """
**当前支持的图表类型: bar, line, pie, liquid, wordcloud**

```pyecharts
{
    "chart": "liquid",
    "title": "水球图",
    "size": [800, 400],
    "data": 0.891
}
```
```pyecharts
{
    "chart": "bar",
    "title": "柱状图",
    "size": [800, 400],
    "data": [
        ["Sam S Club", 10000],
        ["Amy Schumer", 4386],
        ["Charter Communications", 2467],
        ["Planet Fitness", 1868],
        ["Express", 1112],
        ["Johnny Depp", 847],
        ["Lewis Hamilton", 555],
        ["Mary Ellen Mark", 462],
        ["Rita Ora", 360],
        ["NCAA baseball tournament", 273]
    ]
}
```
"""
    }


class Cache:
    resource_dir = os.path.join(f'{os.path.dirname(__file__)}', 'resource')
    img_dir = os.path.join(resource_dir, 'img')
    temp_dir = os.path.join(resource_dir, 'temp')
    examples_md = os.path.join(resource_dir, 'MdxEditor_Examples.md')
    miniblink_dll = os.path.join(resource_dir, 'miniblink_x64.dll')
    dark_css = os.path.join(resource_dir, 'dark.css')
    light_css = os.path.join(resource_dir, 'light.css')
    files_json = os.path.join(resource_dir, 'files.json')
    http_res_dir = "http://nextroad.com.cn:9000/mdx/"
    icon_url = http_res_dir + "mdx.png"    # html ico
    local_mermaid_js = os.path.join(resource_dir, 'mermaid-9.2.1.min.js')
    md_file_path = ''
    dark_code = 'monokai'
    light_code = 'manni'
    dark_theme = False
    css_style = dark_css if dark_theme else light_css
    code_style = dark_code if dark_theme else light_code


class Utils:

    @classmethod
    def get_time(cls):
        return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())

    @staticmethod
    def create_thread(func, args=()) -> threading.Thread:
        th = threading.Thread(target=func, args=args)
        th.daemon = True
        th.start()
        return th

    @staticmethod
    def json_read(path: str):
        try:
            with open(path, 'r', encoding='utf-8') as f:
                return json.load(f)
        except Exception as e:
            print(f"json read failed: {str(e)}")
            return None

    @staticmethod
    def json_write(path, data) -> bool:
        try:
            with open(path, 'w+', encoding='utf-8') as f:
                f.write(json.dumps(data, indent=4, ensure_ascii=False))
        except Exception as e:
            print(f"json write failed: {str(e)}")
            return False
        return True


class WebViewer(object):
    """
    Miniblink: https://www.miniblink.net/views/doc/api-doc.html
    """

    def __init__(self, master: tk.Frame, width: int, height: int):
        """
        master: tk.Frame控件, miniblink窗口显示在此窗口句柄上 (ctk.CTkFrame控件上布局无法显示，原因暂时未知)
        width: 窗口的宽度
        height: 窗口的高度
        """
        self.width = width
        self.height = height
        self.master = master
        self.mb_dll = Miniblink.init(Cache.miniblink_dll)
        miniblink = Miniblink(self.mb_dll)
        self.window = miniblink.window
        self.runjs = miniblink.pyrunjs
        self.webview = None

    def init_viewer(self):
        self.webview = self.window.wkeCreateWebWindow(2, self.master.winfo_id(), 0, 0, self.width, self.height)

    def load_file(self, file_path: str) -> None:
        if not os.path.isfile(file_path):
            return
        with open(file_path, 'r', encoding='utf-8') as f:
            text = f.read()
        if file_path.endswith('.html'):
            self.load_html(text)
        elif file_path.endswith('.md'):
            self.load_html(MarkdownRender.to_html(text))

    def load_html(self, html_data: str) -> None:
        self.init_viewer()
        self.mb_dll.wkeLoadHTMLW(self.webview, html_data)
        self.show()

    def load_url(self, url: str) -> None:
        self.init_viewer()
        self.mb_dll.wkeLoadURLW(self.webview, url)
        self.show()

    def show(self) -> None:
        self.window.wkeShowWindow(self.webview)

    def destroy(self) -> None:
        if self.webview is not None:
            # self.window.wkeDestroyWebView(self.webview)
            self.master.destroy()
            self.webview = None
        del self

    def run_js(self, js):
        self.runjs.run_js(self.webview, js)

    def set_zoom(self, size):
        if self.webview:
            self.window.wkeSetZoomFactor(self.webview, size)


class JavaScript:
    go_top = '''window.scrollTo(0, 0);'''
    go_bottom = '''var docHeight = document.documentElement.scrollHeight;
window.scrollTo(0, docHeight);'''
    onload_gobottom = '''<script type="text/javascript">window.onload = function() {
var docHeight = document.documentElement.scrollHeight;
window.scrollTo(0, docHeight);
};</script>'''
    copy_btn_template = "document.getElementById('copy_btn_%s').onclick = function() {copyCode(this, 'code_%s');};"
    copy_btn_func = '''function copyCode(btn, codeId) {
    btn.textContent = '已复制';
    setTimeout(function() {btn.textContent = '复制'; }, 1000); 
    window.getSelection().removeAllRanges();  
    var range = document.createRange();  
    range.selectNode(document.getElementById(codeId));  
    var selection = window.getSelection();  
    selection.addRange(range);
    document.execCommand('copy');
    window.getSelection().removeAllRanges(); 
}'''


class MarkdownRender:
    """
    Markdown渲染器
    支持生成暗黑/明亮两种风格html
    """
    code_titles = None

    @classmethod
    def to_html(cls, txt: str) -> str:
        """
        markdown文本转换Html内容
        txt: markdown内容
        :return: 返回html内容，包含嵌入了的图片及CSS样式表
        """
        cls.code_titles = []
        txt = cls._code_render('prev', txt)
        txt = cls._change_img_width(txt)
        txt = cls._mermaid_render(txt)
        txt = cls._pyecharts_render(txt)
        html = cls._html_render(txt)
        html = cls._code_render('fina', html)
        html = cls._add_css(html)
        html = cls._add_js(html)
        html = cls.for_local(html)
        return html

    @classmethod
    def for_local(cls, html):
        for http_js in set(re.findall(r'https?://[^\s"\']+\.js(?=["&])', html)):
            local_js = os.path.join(Cache.resource_dir, http_js.split('/')[-1])
            if os.path.isfile(local_js):
                html = html.replace(http_js, local_js)
            else:
                print(f'warn: {http_js} not fount in local, pls check')
        return html

    @classmethod
    def for_export(cls, html: str) -> str:
        """ 导出文档前的最终处理，将本地js文件路径替换为CDN等网络路径 """
        escaped_path = re.escape(Cache.resource_dir)  # 转义特殊字符
        for local_js in set(re.findall(fr'{escaped_path}\\[^\\\s]+\.js(?=["&])', html)):
            http_js = Cache.http_res_dir + local_js.split('\\')[-1]
            html = html.replace(local_js, http_js)
        return html

    @classmethod
    def _html_render(cls, txt: str) -> str:
        extensions = [
            # markdown-extensions #
            'toc', 'tables', 'admonition', 'codehilite', 'fenced_code', 'nl2br',
            'sane_lists', 'attr_list', 'footnotes',
            # pymdown-extensions #
            'pymdownx.progressbar', 'pymdownx.tasklist', 'pymdownx.magiclink', 'pymdownx.b64', 'pymdownx.mark',
            'pymdownx.blocks.details', 'pymdownx.blocks.html', 'pymdownx.blocks.tab', 'pymdownx.blocks.admonition', 'pymdownx.emoji', 
            # 3rd-extensions #
            'markdown_del_ins', 'grid_tables',
        ]
        extension_configs = {
            'codehilite': {'pygments_style': Cache.code_style, 'noclasses': True},
            'pymdownx.tasklist': {'custom_checkbox': True},
            'pymdownx.b64': {'base_path': Cache.resource_dir},
        }
        return markdown.markdown(txt, extensions=extensions, extension_configs=extension_configs)

    @classmethod
    def _code_render(cls, step: str, txt: str) -> str:
        """ 渲染代码块，给代码块增加客制化样式 """
        code_title_template = '<span class="codehilite-title">%s</span>'
        if step == 'prev':
            lines = []
            for line in txt.split('\n'):
                if line.startswith('```') and line.strip('```').strip() != '':
                    code_type = line.strip('```').split(' ')[0]
                    if code_type in ('mermaid', 'pyecharts'):
                        lines.append(line)
                        continue
                    params = 'hl_lines="%s"' % line.split('hl_lines=')[1].split('"')[1] if 'hl_lines=' in line else ''
                    title = line.split('title=')[1].split('"')[1] if 'title=' in line else code_type 
                    lines.append(f'{code_title_template % title}\n```{code_type} {params}')
                    cls.code_titles.append(title)
                    continue
                lines.append(line)
            txt = '\n'.join(lines)
        elif step == 'fina':
            # 重新修改明暗主题(monokai & manni)下代码的背景色
            colors = [
                '<div class="codehilite" style="background: #272822">',
                '<div class="codehilite" style="background: #f0f3f3">',
                '<div class="codehilite" style="background: #1a1a1a">', 
                '<div class="codehilite" style="background: #f1f2f2">'
            ]
            counter = [0, 0]
            txt = txt.replace(colors[0], colors[2]).replace(colors[1], colors[3])
            for title in set(cls.code_titles):
                txt = txt.replace(f'<p>{code_title_template % title}</p>\n{colors[2]}',
                                  f'{colors[2]}<p class="title">{title}</p>'
                        ).replace(f'<p>{code_title_template % title}</p>\n{colors[3]}',
                                  f'{colors[3]}<p class="title">{title}</p>')
            
            def add_btn_id(match):
                counter[0] += 1
                return f'''{match.group(0).rstrip('</p>')}<button id="copy_btn_{counter[0]}" class="copy-button">复制</button></p>'''

            def add_code_id(match):
                counter[1] += 1
                return match.group(0).replace('<code>', f'<code id="code_{counter[1]}">')
            txt = re.sub(r'<p class="title">(.*?)</p>', add_btn_id, txt)
            txt = re.sub(r'<pre style="line-height: 125%;"><span></span><code>', add_code_id, txt) 
        return txt

    @classmethod
    def _mermaid_render(cls, txt: str) -> str:
        """ 渲染Mermaid图表 """
        match = re.findall(r'```mermaid\n(.*?)\n```', txt, re.DOTALL)
        if not match:
            return txt
        prev = """<script src="%s"></script>
<script>mermaid.initialize({
    startOnLoad:true, 
    theme: "%s", 
    themeVariables: {
        fontFamily: "微软雅黑, sans-serif",
        fontSize: "12px"
    }});
</script>
""" % (Cache.local_mermaid_js, "dark" if Cache.dark_theme else "default")
        txt = prev + txt
        for item in set(match):
            txt = txt.replace(f'```mermaid\n{item}\n```', f'<div class="mermaid">\n{item}\n</div>')
        return txt

    @classmethod
    def _pyecharts_render(cls, txt: str) -> str:
        """ 渲染Pyecharts图表 """
        def get_chart_inst():
            init_opts = opts.InitOpts(
                width=f'{size[0]}px', height=f'{size[1]}px',
                theme=ThemeType.CHALK if Cache.dark_theme else ThemeType.WHITE)
            if chart == 'bar':
                data_d = OrderedDict(data)
                _inst = Bar(init_opts=init_opts)
                _inst.add_xaxis(list(data_d.keys()))
                _inst.add_yaxis("", list(data_d.values()))
                _inst.set_global_opts(
                    title_opts=opts.TitleOpts(title=title, pos_left='center'),
                    legend_opts=opts.LegendOpts(is_show=False),
                    xaxis_opts=opts.AxisOpts(
                        axislabel_opts=opts.LabelOpts(rotate=-15),
                        splitline_opts=opts.SplitLineOpts(is_show=False),
                    ),
                    yaxis_opts=opts.AxisOpts(
                        splitline_opts=opts.SplitLineOpts(is_show=False),
                    ))
            elif chart == 'line':
                data_d = OrderedDict(data)
                _inst = Line(init_opts=init_opts)
                _inst.add_xaxis(list(data_d.keys()))
                _inst.add_yaxis("", list(data_d.values()), is_connect_nones=True, symbol_size=10,
                                linestyle_opts=opts.LineStyleOpts(width=3))
                _inst.set_global_opts(
                    title_opts=opts.TitleOpts(title=title, pos_left='center'),
                    legend_opts=opts.LegendOpts(is_show=False),
                    tooltip_opts=opts.TooltipOpts(trigger="axis"),
                    xaxis_opts=opts.AxisOpts(
                        type_="category",
                        axislabel_opts=opts.LabelOpts(rotate=-15),
                        splitline_opts=opts.SplitLineOpts(is_show=False),
                        boundary_gap=False
                    ),
                    yaxis_opts=opts.AxisOpts(
                        type_="value",
                        axistick_opts=opts.AxisTickOpts(is_show=True),
                        splitline_opts=opts.SplitLineOpts(is_show=True),
                    ))
            elif chart == 'pie':
                _inst = Pie(init_opts=init_opts)
                _inst.add('', data)
                _inst.set_global_opts(
                    title_opts=opts.TitleOpts(title=title, pos_left='center'),
                    legend_opts=opts.LegendOpts(is_show=False))
                _inst.set_series_opts(tooltip_opts=opts.TooltipOpts(trigger="item", formatter="{b}: {c} ({d}%)"))
            elif chart == 'wordcloud':
                _inst = WordCloud(init_opts=init_opts)
                _inst.add("", data, shape=SymbolType.ROUND_RECT)
                _inst.set_global_opts(
                    title_opts=opts.TitleOpts(title=title, pos_left='center'),
                    legend_opts=opts.LegendOpts(is_show=False))
            elif chart == 'liquid':
                _inst = Liquid(init_opts=init_opts)
                _inst.add("", [float(data)], is_outline_show=False)
                _inst.set_global_opts(
                    title_opts=opts.TitleOpts(title=title, pos_left='center'),
                    legend_opts=opts.LegendOpts(is_show=False))
            else:
                _inst = None
            return _inst

        match = re.findall(r'```pyecharts\n(.*?)\n```', txt, re.DOTALL)
        if not match:
            return txt
        for item in set(match):
            try:
                json_data = json.loads(item)
                title = json_data.get('title')
                size = json_data.get('size', [800, 400])
                chart = json_data['chart']
                data = json_data['data']
                inst = get_chart_inst()
                if inst is None:
                    continue
                html = inst.render_embed().replace('"', '&quot;')
                iframe = (f'<iframe srcdoc="{html}" width="{size[0]}" height="{size[1]}" '
                          f'scrolling="no" frameborder="0" >Err</iframe>')
                txt = txt.replace(f'```pyecharts\n{item}\n```', iframe)
            except:
                continue
        return txt

    @classmethod
    def _change_img_width(cls, txt: str) -> str:
        """ 增加对图片宽高设置的功能
        格式：
            ![](xxx/aa.png =800x400)    # 严格上的宽高
            ![](xxx/aa.png =800x0)      # 按照（宽800/原始宽）的比例缩放高度
            ![](xxx/aa.png =0x400)      # 按照（高400/原始高）的比例缩放宽度
            ![](xxx/aa.png =0x0)        # 原始不压缩，等同于不加=号
        """
        match = re.findall(r'!\[]\((\S+)\)', txt)
        if match:
            for item in set(match):
                if os.path.isfile(item):
                    new_path = os.path.abspath(item).replace('\\', '/')
                    txt = txt.replace(f'![]({item})', f"![]({new_path})")
        match = re.findall(r'!\[]\((\S+) =(\d+)x(\d+)\)', txt)
        if not match:
            return txt
        for item in set(match):
            try:
                path = item[0]
                width = int(item[1])
                height = int(item[2])
            except:
                continue
            if (not os.path.isfile(path)) or width < 0 or height < 0:
                continue
            tmp_path = os.path.join(Cache.temp_dir, f'{width}_{height}_{os.path.basename(path)}')
            if not os.path.isfile(tmp_path):
                img = Image.open(path)
                if width == 0 and height > 0:
                    width = int(height / img.height * img.width)
                elif height == 0 and width > 0:
                    height = int(width / img.width * img.height)
                elif width == 0 and height == 0:
                    width = img.width
                    height = img.height
                img.resize((width, height)).save(tmp_path)
            tmp_path = tmp_path.replace('\\', '/')
            txt = txt.replace(f'![]({path} ={item[1]}x{item[2]})', f'![]({tmp_path})')
        return txt

    @classmethod
    def _add_css(cls, html: str) -> str:
        """ 嵌入CSS样式 """
        match = re.findall(r'<h1 id=".*?">(.*?)</h1>', html)
        if match:
            title = match[0]
        else:
            title = os.path.basename(Cache.md_file_path).rstrip('.md') if Cache.md_file_path else ''
        img_type = {
            'ico': "image/x-icon",
            'jpg': "image/jpeg",
            'jpeg': "image/jpeg",
            'png': "image/png"
        }.get(Cache.icon_url.split('.')[-1], '')
        icon = f'<link rel="icon" type="{img_type}" href="{Cache.icon_url}">' if Cache.icon_url and img_type else ''
        head = f'''<head><meta charset="UTF-8"><title>{title}</title>{icon}</head>'''
        css_file = Cache.css_style
        if not os.path.isfile(css_file):
            Widgets.error_msg(f"{css_file} 不存在，请确认！")
            return '%s\n%s' % (head, html)
        with open(css_file, 'r', encoding='utf-8') as f:
            html = '%s\n<style>%s</style>\n%s' % (head, f.read(), html)
        return html

    @classmethod
    def _add_js(cls, html: str) -> str:
        """ 嵌入javascript """
        prev = '\n<script>'
        for idx in range(len(cls.code_titles)):
            prev += '\n' + JavaScript.copy_btn_template % (idx+1, idx+1)
        return html + prev + JavaScript.copy_btn_func + '</script>'


class Widgets:
    _toplevel = None

    @classmethod
    def error_msg(cls, text):
        messagebox.showerror('Error', text)

    @classmethod
    def widget_tip(cls, widget, text, width=22, height=22):
        def enter(event=None):
            if cls._toplevel:
                return
            x, y, cx, cy = widget.bbox("insert")
            x = x + widget.winfo_rootx() + width
            y = y + widget.winfo_rooty() - height
            cls._toplevel = tw = tk.Toplevel(widget)
            tw.overrideredirect(1)
            tw.wm_attributes('-topmost', 1)
            tw.geometry("+%d+%d" % (x, y))
            tk.Label(tw, text=text, font=("tahoma", "9"), justify='left', background="#ffffe0",
                     relief=tk.SOLID, borderwidth=1).pack(ipadx=1)

        def leave(event=None):
            if cls._toplevel:
                cls._toplevel.destroy()
                cls._toplevel = None

        widget.bind('<Enter>', enter)
        widget.bind('<Leave>', leave)

    @classmethod
    def scale_adjust(cls, master, tip, values, dft_idx, handler):
        def up():
            idx = values.index(lab.cget('text'))
            if idx == len(values) - 1:
                return
            val = values[idx + 1]
            lab.configure(text=str(val))
            handler(val)

        def down():
            idx = values.index(lab.cget('text'))
            if idx == 0:
                return
            val = values[idx - 1]
            lab.configure(text=str(val))
            handler(val)

        lab = ctk.CTkLabel(
            master, text=values[dft_idx], text_color=('black', 'white'), width=40, height=24, corner_radius=0,
            fg_color='transparent', justify='right', anchor='e')
        lab.grid(row=0, column=0, rowspan=2, pady=1)
        cls.widget_tip(lab, tip)
        ctk.CTkButton(
            master, text='▲', width=18, height=11, hover_color='darkorange', fg_color='transparent',
            text_color=('black', 'white'), border_spacing=0, corner_radius=0, font=ctk.CTkFont(size=10, family='黑体'),
            command=up).grid(row=0, column=1, pady=(1, 0))
        ctk.CTkButton(
            master, text='▼', width=18, height=11, hover_color='darkorange', fg_color='transparent',
            text_color=('black', 'white'), border_spacing=0, corner_radius=0, font=ctk.CTkFont(size=10, family='黑体'),
            command=down).grid(row=1, column=1, pady=(0, 1))

    @classmethod
    def menu_buttons(cls, master, items, width, height, handler, txt=True, side='left', tip=True, pady=0, font=None):
        for idx, item in enumerate(items):
            text, img = item
            btn = ctk.CTkButton(
                master, image=img, text=text if txt else '', text_color=('black', 'white'), width=width, height=height,
                fg_color='transparent', hover_color='darkorange', border_width=0, corner_radius=0,
                font=font, command=lambda x=text: handler(x))
            btn.pack(side=side, pady=pady, padx=(2, 0) if idx == 0 else 0)
            if tip:
                cls.widget_tip(btn, text)


class TextEditor(object):

    def __init__(self, master):
        self.number_text = ctk.CTkTextbox(
            master, width=42, activate_scrollbars=False, fg_color=('gray99', 'gray15'), corner_radius=0,
            font=ctk.CTkFont(size=13, family='DejaVu Sans Mono'), text_color='gray60', state='disabled')
        self.number_text.pack(side="left", fill='y')
        self.edit_text = ctk.CTkTextbox(
            master, font=ctk.CTkFont(size=13, family='DejaVu Sans Mono'), fg_color=('white', 'gray10'),
            corner_radius=0, undo=True, wrap="none")   # wrap="none"确保水平滚动可见
        self.edit_text.pack(side='left', expand=True, fill="both")
        self.edit_text.configure(yscrollcommand=self._sync_scroll)
        self.edit_text.bind("<KeyRelease>", self.highlight_line, add=True)
        self.edit_text.bind("<ButtonRelease>", self.highlight_line, add=True)
        self.edit_text.bind("<Configure>", self._update_highlight, add=True)  # 窗口大小改变时更新
        self._update_highlight()

    def highlight_line(self, event=None):
        """高亮整行（包括行首到行尾的空白区域）"""
        line_num = self.edit_text.get("1.0", "end").count('\n')
        numbers = "\n".join([f"{str(i + 1):>4}" for i in range(line_num)])
        self.number_text.configure(state="normal")
        self.number_text.delete("1.0", "end")
        self.number_text.insert("1.0", numbers)
        self.number_text.configure(state="disabled")
        self.number_text.yview_moveto(self.edit_text.yview()[0])
        self.edit_text.tag_remove("current_line", "1.0", "end")
        curr_line = self.edit_text.index(ctk.INSERT).split('.')[0]
        self.edit_text.tag_config(
            "current_line", background='lightblue' if ctk.get_appearance_mode() == 'Light' else 'darkblue')
        self.edit_text.tag_add("current_line", f"{curr_line}.0", f"{curr_line}.0 lineend +1c")
        self.edit_text.see(ctk.INSERT)

    def _sync_scroll(self, *args):
        pos = self.edit_text.yview()[0]
        self.number_text.yview_moveto(pos)
        self.edit_text._y_scrollbar.set(*args)

    def _update_highlight(self, event=None):
        """更新高亮区域宽度以填充整个控件"""
        # 设置高亮标签的左右边距
        self.edit_text.tag_config("current_line",
                                  lmargin1=0,  # 行首缩进
                                  lmargin2=0,  # 换行缩进
                                  rmargin=self.edit_text.winfo_width())  # 右边距到控件边缘


class AboutPage:
    _top = None

    @classmethod
    def pack(cls):
        if cls._top:
            cls._top.wm_attributes('-topmost', 1)
            return
        url = 'https://gitee.com/BingoLee1/MdxEditor'
        text = '''一款现代化的技术类文档编辑器。
%s

Copyright@2025 BingoLee
''' % url
        cls._top = ctk.CTkToplevel(fg_color=('white', 'black'))
        cls._top.title('关于 MdxEditor')
        cls._top.geometry(f'300x200+{(cls._top.winfo_screenwidth() - 300)//2}+{(cls._top.winfo_screenheight()-200)//2}')
        cls._top.resizable(False, False)
        cls._top.wm_attributes('-topmost', 1)
        cls._top.protocol("WM_DELETE_WINDOW", cls.close)
        cls.img = ctk.CTkImage(Image.open(os.path.join(Cache.img_dir, 'app.ico')), size=(50, 50))
        ctk.CTkLabel(cls._top, image=cls.img, text='Mdx Editor', compound='top',
                     font=ctk.CTkFont(size=18, family='微软雅黑', weight='bold')).pack(pady=20)
        lab = ctk.CTkLabel(cls._top, text=text, text_color=('blue', 'lightblue'))
        lab.pack()
        lab.bind('<Button-1>', lambda event: webbrowser.open(url))

    @classmethod
    def close(cls):
        if not cls._top:
            return
        cls._top.destroy()
        cls._top = None


class App(ctk.CTk):
    _title_prefix = f'Mdx Editor v{VERSION}'

    def __init__(self):
        ctk.CTk.__init__(self)
        self.title(self._title_prefix)
        screen_w, screen_h = self.winfo_screenwidth(), self.winfo_screenheight()
        if (screen_w >= 1920 and screen_h == 1080) or (screen_w == 1920 and screen_h >= 1080):
            self.state('zoomed')
        elif screen_w > 1920 and screen_h > 1080:
            x = int((screen_w - 1920) / 2)
            y = int((screen_h - 1080) / 2)
            self.geometry(f'1920x1080+{x}+{y}')
            self.maxsize(1920, 1080)
        self.minsize(1366, 768)
        self.update()
        self.iconbitmap(os.path.join(Cache.img_dir, 'app.ico'))
        self.protocol("WM_DELETE_WINDOW", self.close)
        self.files_width = 140
        self.min_width = (800, 920)  # 最小宽度设置 (编辑宽度，预览宽度)
        self.foot_height = 16
        self.show_height = self.winfo_height() - self.foot_height - 35
        self.show_master = None
        self.files_master = None
        self.webview = None
        self.text_editor = None
        self.head_tip_label = None
        self.foot_tip_label = None
        self.load_img()

    def load_img(self):
        # menu btn
        self.img_new_md = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'new_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'new_0.png')), size=(22, 22))
        self.img_open_md = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'open_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'open_0.png')), size=(22, 22))
        self.img_export = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'export_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'export_0.png')), size=(22, 22))
        self.img_theme = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'theme_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'theme_0.png')), size=(22, 22))
        self.img_about = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'about_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'about_0.png')), size=(22, 22))
        self.img_help = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'help_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'help_0.png')), size=(22, 22))
        # eg btn
        self.img_biaoti = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'biaoti_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'biaoti_0.png')), size=(16, 16))
        self.img_cuti = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'cuti_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'cuti_0.png')), size=(16, 16))
        self.img_xieti = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'xie_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'xie_0.png')), size=(16, 16))
        self.img_xiecu = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'xiecu_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'xiecu_0.png')), size=(16, 16))
        self.img_shanchu = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'shanchu_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'shanchu_0.png')), size=(16, 16))
        self.img_xiahua = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'xiahua_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'xiahua_0.png')), size=(16, 16))
        self.img_beijing = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'beijing_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'beijing_0.png')), size=(16, 16))
        self.img_yinyong = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'yinyong_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'yinyong_0.png')), size=(16, 16))
        self.img_daimakuai = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'daimakuai_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'daimakuai_0.png')), size=(16, 16))
        self.img_hangdaima = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'hangdaima_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'hangdaima_0.png')), size=(16, 16))
        self.img_konghang = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'konghang_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'konghang_0.png')), size=(16, 16))
        self.img_biaoge = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'table_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'table_0.png')), size=(16, 16))
        self.img_fuzabiao = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'fuzabiao_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'fuzabiao_0.png')), size=(16, 16))
        self.img_youxubiao = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'youxubiao_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'youxubiao_0.png')), size=(16, 16))
        self.img_wuxubiao = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'wuxubiao_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'wuxubiao_0.png')), size=(16, 16))
        self.img_renwubiao = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'renwubiao_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'renwubiao_0.png')), size=(16, 16))
        self.img_tishi = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'tip_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'tip_0.png')), size=(16, 16))
        self.img_zhuyi = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'note_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'note_0.png')), size=(16, 16))
        self.img_jinggao = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'jinggao_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'jinggao_0.png')), size=(16, 16))
        self.img_cuowu = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'cuowu_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'cuowu_0.png')), size=(16, 16))
        self.img_line = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'line_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'line_0.png')), size=(16, 16))
        self.img_jindutiao = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'jindutiao_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'jindutiao_0.png')), size=(16, 16))
        self.img_zhedie = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'zhedie_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'zhedie_0.png')), size=(16, 16))
        self.img_biaoqian = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'tab_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'tab_0.png')), size=(16, 16))
        self.img_image = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'image_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'image_0.png')), size=(16, 16))
        self.img_button = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'button_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'button_0.png')), size=(16, 16))
        self.img_juzhong = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'juzhong_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'juzhong_0.png')), size=(16, 16))
        self.img_mulu = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'mulu_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'mulu_0.png')), size=(16, 16))
        self.img_jiaozhu = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'jiaozhu_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'jiaozhu_0.png')), size=(16, 16))
        self.img_mermaid = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'mermaid_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'mermaid_0.png')), size=(16, 16))
        self.img_echarts = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'echarts_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'echarts_0.png')), size=(16, 16))
        # view btn
        self.img_gotop = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'gotop_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'gotop_0.png')), size=(22, 22))
        self.img_gobottom = ctk.CTkImage(
            light_image=Image.open(os.path.join(Cache.img_dir, 'gobottom_1.png')),
            dark_image=Image.open(os.path.join(Cache.img_dir, 'gobottom_0.png')), size=(22, 22))

    def pack(self):
        edit_master = ctk.CTkFrame(
            self, corner_radius=0, border_width=1, border_color='white', fg_color='transparent')
        show_master = ctk.CTkFrame(
            self, corner_radius=0, border_width=1, border_color='white', fg_color='transparent', width=self.min_width[1])
        foot_master = ctk.CTkFrame(
            self, corner_radius=0, border_width=1, border_color='white', fg_color='transparent', height=self.foot_height)
        edit_master.grid(row=0, column=0, sticky='nsew')
        show_master.grid(row=0, column=1, sticky='nsew')
        foot_master.grid(row=1, column=0, columnspan=2, sticky='nsew')
        self.pack_edit_area(edit_master)
        self.pack_show_area(show_master)
        self.pack_foot_area(foot_master)
        self.grid_columnconfigure(0, weight=1)
        self.grid_rowconfigure(0, weight=1)

    def close(self):
        if Cache.md_file_path:
            text = self.text_editor.edit_text.get('1.0', 'end')
            with open(Cache.md_file_path, 'w', encoding='UTF-8') as file:
                file.write(text)
        self.quit()

    def pack_show_area(self, master):
        head_frame = ctk.CTkFrame(
            master, corner_radius=0, width=self.min_width[1], border_width=1, border_color='white',
            fg_color=('gray85', 'gray25'))
        self.show_master = ctk.CTkFrame(
            master, corner_radius=0, width=self.min_width[1], height=self.show_height,
            border_width=1, border_color='white', fg_color=('white', 'black'))
        head_frame.grid(row=0, column=0, sticky='nsew', padx=1)
        self.show_master.grid(row=1, column=0)
        master.grid_rowconfigure(0, weight=1)
        master.grid_columnconfigure(0, weight=1)
        btn_items = (
            ('回顶部', self.img_gotop),
            ('到底部', self.img_gobottom)
        )
        Widgets.menu_buttons(head_frame, btn_items, 40, 28, self.view_handler, txt=False)
        self.head_tip_label = ctk.CTkLabel(
            head_frame, text='新文件', font=ctk.CTkFont(size=12, family='微软雅黑'), text_color=('gray45', 'gray65'),
            fg_color='transparent')
        self.head_tip_label.pack(side='right', padx=10, pady=2)

        def set_html_zoom(size):
            if self.webview:
                self.webview.set_zoom(int(str(size).strip('%')) / 100)

        scale_fm = ctk.CTkFrame(head_frame, height=26, corner_radius=0, fg_color='transparent')
        scale_fm.pack(side='left', pady=1)
        Widgets.scale_adjust(scale_fm, '缩放文档', list(map(lambda x: str(x*10)+'%', range(2, 21))), 8, set_html_zoom)

    def pack_edit_area(self, master):
        menu_master = ctk.CTkFrame(
            master, fg_color=('gray85', 'gray25'), width=self.min_width[0], corner_radius=0,
            border_width=1, border_color='white',)
        self.files_master = ctk.CTkFrame(
            master, width=self.files_width, corner_radius=0, fg_color=('gray99', 'gray20'))
        edit_master = ctk.CTkFrame(
            master, corner_radius=0, fg_color=('gray95', 'gray15'), border_width=1, border_color='white')
        menu_master.grid(row=0, column=0, columnspan=2, sticky='nsew')
        self.files_master.grid(row=1, column=0, sticky='nsew', padx=(1, 0))
        edit_master.grid(row=1, column=1, sticky='nsew')
        self.pack_menubar(menu_master)
        self.pack_textbox(edit_master)
        master.grid_rowconfigure(1, weight=1)
        master.grid_columnconfigure(0, weight=1)
        master.grid_columnconfigure(1, weight=4, minsize=self.min_width[0])

    def pack_menubar(self, master):
        btn_items = (
            ('新建', self.img_new_md),
            ('打开', self.img_open_md),
            ('导出', self.img_export),
            ('主题', self.img_theme)
        )
        Widgets.menu_buttons(
            master, btn_items, 80, 28, self.button_handler, tip=False, pady=1,
            font=ctk.CTkFont(size=15, family='微软雅黑', weight='bold'))
        btn_items = (
            ('关于', self.img_about),
            ('帮助', self.img_help)
        )
        Widgets.menu_buttons(master, btn_items, 40, 28, self.button_handler, txt=False, side='right', pady=1)

        def set_ui_scale(size):
            ctk.set_widget_scaling(int(str(size).strip('%')) / 100)

        scale_fm = ctk.CTkFrame(master, height=26, corner_radius=0, fg_color='transparent')
        scale_fm.pack(side='right', pady=1)
        Widgets.scale_adjust(scale_fm, '缩放UI控件', list(map(lambda x: str(x*10)+'%', range(5, 16))), 5, set_ui_scale)

    def pack_filesbar(self, files, select=True):
        for child in self.files_master.winfo_children():
            child.destroy()
        ctk.CTkLabel(
            self.files_master, font=ctk.CTkFont(size=12, family='微软雅黑'), text='历史文件',
            height=26, fg_color=('gray94', 'gray22')
        ).pack(fill='x', padx=1, pady=1)
        for idx, file in enumerate(files):
            _, name = os.path.split(file)
            btn = ctk.CTkButton(
                self.files_master, text=name, font=ctk.CTkFont(size=11, family='微软雅黑'), text_color=('black', 'white'),
                height=22, hover_color='darkorange', anchor='w', border_width=0,
                corner_radius=0, command=lambda x=file: self.open_file(x))
            btn.pack(anchor='w', fill='x', padx=1)
            Widgets.widget_tip(btn, file)
            if select and idx == 0:
                btn.configure(fg_color='tan')
            else:
                btn.configure(fg_color='transparent')

    def pack_textbox(self, master):
        btns_fm = ctk.CTkFrame(master, fg_color=('gray95', 'gray15'), corner_radius=0)
        btns_fm.grid(row=0, column=0, sticky='nsew', padx=1, pady=1)
        text_fm = ctk.CTkFrame(master, corner_radius=0)
        text_fm.grid(row=1, column=0, sticky='nsew', padx=1)
        master.grid_rowconfigure(1, weight=1)
        master.grid_columnconfigure(0, weight=1)
        btn_items = (
            ('标题', self.img_biaoti),
            ('粗体', self.img_cuti),
            ('斜体', self.img_xieti),
            ('斜体加粗', self.img_xiecu),
            ('删除文字', self.img_shanchu),
            ('下划线', self.img_xiahua),
            ('水平线', self.img_line),
            ('背景高亮', self.img_beijing),
            ('中心对齐', self.img_juzhong),
            ('空行', self.img_konghang),
            ('目录', self.img_mulu),
            ('脚注', self.img_jiaozhu),
            ('引用', self.img_yinyong),
            ('进度条', self.img_jindutiao),
            ('折叠块', self.img_zhedie),
            ('标签页', self.img_biaoqian),
            ('代码块', self.img_daimakuai),
            ('行代码', self.img_hangdaima),
            ('按钮', self.img_button),
            ('图片', self.img_image),
            ('提示', self.img_tishi),
            ('注意', self.img_zhuyi),
            ('警告', self.img_jinggao),
            ('错误', self.img_cuowu),
            ('有序列表', self.img_youxubiao),
            ('无序列表', self.img_wuxubiao),
            ('任务列表', self.img_renwubiao),
            ('表格', self.img_biaoge),
            ('复杂表格', self.img_fuzabiao),
            ('Mermaid', self.img_mermaid),
            ('Echarts', self.img_echarts),
        )
        Widgets.menu_buttons(btns_fm, btn_items, 20, 20, self.button_handler, txt=False, pady=1)
        self.text_editor = TextEditor(text_fm)
        self.text_editor.edit_text.bind("<Enter>", self.on_enter_event)
        self.text_editor.edit_text.bind("<Control-Key-n>", self.on_create_event)
        self.text_editor.edit_text.bind("<Control-Key-s>", self.on_save_event)
        self.text_editor.edit_text.bind("<Control-Shift-S>", self.on_export_event)

    def pack_foot_area(self, master):
        self.foot_tip_label = ctk.CTkLabel(
            master, text='', height=self.foot_height, font=ctk.CTkFont(size=10, family='微软雅黑'), fg_color='transparent')
        self.foot_tip_label.grid(row=0, column=0, padx=1, pady=1, sticky='nsew')
        self.foot_tip_label.configure(text="Tip: `Ctrl+s`保存内容到文件; `Ctrl+Shift+s`导出到HTML文件")
        master.grid_columnconfigure(0, weight=1)

    def view_handler(self, text):
        if text == '回顶部':
            if self.webview:
                self.webview.run_js(JavaScript.go_top)

        elif text == '到底部':
            if self.webview:
                self.webview.run_js(JavaScript.go_bottom)

    def button_handler(self, text):
        export_file = None
        if text == '新建':
            if not self.on_create_event():
                return
        elif text == '打开':
            if not self.on_open_event():
                return
        elif text == '导出':
            export_file = self.on_export_event()
            if not export_file:
                return
        elif text == '主题':
            Cache.dark_theme = not Cache.dark_theme
            if Cache.dark_theme:
                ctk.set_appearance_mode('dark')
                Cache.code_style = Cache.dark_code
                Cache.css_style = Cache.dark_css
            else:
                ctk.set_appearance_mode('light')
                Cache.code_style = Cache.light_code
                Cache.css_style = Cache.light_css
            self.text_editor.highlight_line()
        elif text == '关于':
            return AboutPage.pack()
        elif text == '帮助':
            return self.open_file(Cache.examples_md)
        else:
            md_str = MD_TEMPLATES.get(text, '')
            if text == '图片':
                file_path = filedialog.askopenfilename(
                    title="Open Image File", filetypes=[('PNG File', '.png'), ('JPG', '.jpg'), ('JPEG', '.jpeg')],
                    initialdir=os.path.dirname(__file__))
                if not file_path:
                    return
                md_str = md_str % file_path
                md_str = md_str.replace('/', '\\\\')
            self.text_editor.edit_text.insert('insert', md_str)
            self.text_editor.highlight_line()
        self.update_webview(export_file=export_file)

    def update_webview(self, export_file=None):
        if Cache.md_file_path:
            os.chdir(os.path.dirname(Cache.md_file_path))
        html = MarkdownRender.to_html(self.text_editor.edit_text.get('1.0', 'end'))
        if export_file:
            html = MarkdownRender.for_export(html)
            with open(export_file, 'w', encoding='UTF-8') as file:
                file.write(html)
            self.update_tips('foot', f"{Utils.get_time()} 已导出至: {export_file}")
        if self.webview is not None:
            self.webview.destroy()
        window = tk.Frame(self.show_master, width=self.min_width[1], height=self.show_height, bg='gray15')
        window.grid(row=0, column=0, sticky="nsew")
        self.webview = WebViewer(window, self.min_width[1], self.show_height)
        self.webview.load_html(html)

    def on_save_event(self, event=None):
        self.update_webview()
        if not Cache.md_file_path:
            file_path = filedialog.asksaveasfilename(
                title="保存新文件", filetypes=[('MD File', '.md')],
                initialfile='新文件.md', initialdir=os.path.dirname(__file__))
            if not file_path:
                return
            Cache.md_file_path = file_path
            self.resort_files(file_path)
            self.update_tips('head', file_path)
        text = self.text_editor.edit_text.get('1.0', 'end').strip()
        with open(Cache.md_file_path, 'w', encoding='UTF-8') as file:
            file.write(text)
        self.update_tips('foot', f"[{Utils.get_time()}] 已保存至: {Cache.md_file_path}")

    def on_create_event(self):
        file_path = filedialog.asksaveasfilename(
            title="创建新文件", filetypes=[('MD File', '.md')],
            initialfile='新文件.md', initialdir=os.path.dirname(__file__))
        if not file_path:
            return False
        Cache.md_file_path = file_path
        with open(Cache.md_file_path, 'w', encoding='UTF-8') as file:
            file.write('')
        self.text_editor.edit_text.delete('1.0', 'end')
        self.update_tips('foot', f"{Utils.get_time()} 已新建文件: {Cache.md_file_path}")
        self.update_tips('head', file_path)
        self.resort_files(file_path)
        return True

    def on_open_event(self, file_path=None):
        if not file_path:
            file_path = filedialog.askopenfilename(
                title="打开md文件", filetypes=[('MD File', '.md')], initialdir=os.path.dirname(__file__))
            if not file_path:
                return False
        with open(file_path, 'r', encoding='UTF-8') as file:
            text = file.read().strip() + '\n'
        Cache.md_file_path = file_path
        self.text_editor.edit_text.delete('1.0', 'end')
        self.text_editor.edit_text.insert('end', text)
        self.text_editor.highlight_line()
        self.update_tips('title', self._title_prefix + f' - {os.path.basename(Cache.md_file_path)}')
        self.update_tips('head', file_path)
        self.update_tips('foot', f"{Utils.get_time()} 已打开文件: {Cache.md_file_path}")
        self.resort_files(file_path)
        return True

    def on_export_event(self, event=None):
        self.update_webview()
        if Cache.md_file_path:
            name = f"{os.path.basename(Cache.md_file_path).replace('.md', '')}.html"
            path = os.path.dirname(Cache.md_file_path)
        else:
            name = '新文件.html'
            path = os.path.dirname(__file__)
        return filedialog.asksaveasfilename(
            title="导出为Html", filetypes=[('Html File', '.html')], initialfile=name, initialdir=path)

    def on_enter_event(self, event=None):
        self.text_editor.edit_text.focus_force()

    def update_tips(self, which, text):
        if which == 'title':
            self.title(text)
        elif which == 'head':
            self.head_tip_label.configure(text=text)
        elif which == 'foot':
            self.foot_tip_label.configure(text=text)

    def resort_files(self, file_path):
        try:
            files = [file for file in Utils.json_read(Cache.files_json) if os.path.isfile(file)][:30]
            if file_path in files:
                files.remove(file_path)
            files.insert(0, file_path)
            Utils.json_write(Cache.files_json, files)
            self.pack_filesbar(files)
        except:
            pass

    def open_file(self, file):
        text = self.text_editor.edit_text.get('1.0', 'end').strip()
        if Cache.md_file_path:
            with open(Cache.md_file_path, 'w', encoding='UTF-8') as f:
                f.write(text)
        elif text:
            Widgets.error_msg("当前编辑内容未保存，请先处理后继续")
            return
        if self.on_open_event(file_path=file):
            self.update_webview()

    def lazy_load(self):
        self.update()
        shutil.rmtree(Cache.temp_dir, ignore_errors=True)
        os.makedirs(Cache.temp_dir, exist_ok=True)
        try:
            files = [file for file in Utils.json_read(Cache.files_json) if os.path.isfile(file)]
        except:
            Utils.json_write(Cache.files_json, [])
            files = []
        self.pack_filesbar(files, select=False)
        if len(sys.argv) > 1 and os.path.isfile(sys.argv[1]):
            self.update()
            self.open_file(sys.argv[1])


def main():
    app = App()
    app.pack()
    app.lazy_load()
    app.mainloop()


if __name__ == '__main__':
    main()
